﻿function SpaRuntime() {

    var self = this;

    var dispatcher = $({});
    
    var routes = new SpaRoutes();

    self.routes = routes;

    var previousHash = null;
    var previousRoute = null;
    var previousArgs = null;

    self.addRoutes = function () {
        for (var i = 0; i < arguments.length; i++) {
            var route = arguments[i];
            routes.addRoute(route);
        }
    };

    self.findRouteByName = function (name) {
        return routes.findRouteByName(name);
    };

    self.getCurrentRoute = function () {
        return previousRoute;
    };

    self.getCurrentArgs = function () {
        return previousArgs;
    };

    self.check = function (force) {
        syncFromBrowserHash(force);
    };

    var silent = 0;
    function syncFromBrowserHash(force) {
        if (silent > 0) return;
        var hash = readHash();
        var route = routes.findRoute(hash);
        if (route) {
            var args = route.extractArgs(hash);
            if (force || previousRoute != route || !utils.theSame(previousArgs, args)) {
                var allowed = true;
                if (previousRoute) {
                    allowed = previousRoute.beforeLeave({
                        proceed: function () {
                            window.location.hash = hash;
                        }
                    });
                }
                if (allowed) {
                    var phash = previousHash;
                    previousRoute = route;
                    previousArgs = args;
                    previousHash = hash;
                    route.process(args, phash);
                    dispatcher.trigger('execute', [route, args, phash]);
                } else {
                    silent++;
                    window.location.hash = previousHash;
                    silent--;
                }
            }
        }
    }

    function readHash() {
        var h = window.location.hash;
        if (h.length > 0 && h.charAt(0) == '#') {
            h = h.substr(1);
        }
        return h;
    }

    function addOnExecute(handler) {
        dispatcher.bind('execute', handler);
    }
    
    $(window).bind('hashchange', function () {
        syncFromBrowserHash(false);
    });

    self.addOnExecute = addOnExecute;

}

function SpaRoutes() {

    var self = this;

    var routes = [];

    this.addRoute = function (route) {
        routes.push(route);
    };

    this.findRoute = function (url) {
        for (var i = 0; i < routes.length; i++) {
            var route = routes[i];
            if (route.matches(url)) {
                return route;
            }
        }
        return null;
    };

    self.findRouteByName = function (name) {
        for (var i = 0; i < routes.length; i++) {
            var route = routes[i];
            if (route.getName() == name) return route;
        }
        return null;
    };

    self.each = function (handler) {
        for (var i = 0; i < routes.length; i++) {
            handler(routes[i]);
        }
    };

    self.eachWithPrefix = function (prefix, handler) {
        self.each(function (route) {
            var name = route.getName();
            if (name.indexOf(prefix) == 0) {
                handler(route);
            }
        });
    };

    (function () {
        for (var i = 0; i < arguments.length; i++) {
            var route = arguments[i];
            self.addRotue(route);
        }
    })();
}

function SpaRoute(name, templateUrl) {

    var self = this;

    var dispatcher = $({});

    this.getTemplateUrl = function () {
        return templateUrl;
    };

    function trimQuery(url) {
        var index = url.indexOf('?');
        if (index >= 0) {
            return url.substr(0, index);
        }
        return url;
    }

    function getQuery(url) {
        var index = url.indexOf('?');
        if (index >= 0) {
            return url.substr(index + 1);
        }
        return '';
    }

    function extractQuery(url) {
        var query = getQuery(url);
        var res = {};
        if (utils.isNullOrWhiteSpace(query)) {
            return res;
        }
        var parts = query.split('&');
        for (var i = 0; i < parts.length; i++) {
            var kv = parts[i].split('=');
            var key = decodeURIComponent(kv[0]);
            var value = decodeURIComponent(kv[1]);
            res[key] = value;
        }
        return res;
    }

    function splitParts(url) {
        var res = [];
        var raw = url.split('/');
        for (var i = 0; i < raw.length; i++) {
            if (raw[i] != '') {
                res.push(raw[i]);
            }
        }
        return res;
    }

    function isVariable(urlPart) {
        return urlPart.charAt(0) == '{' && urlPart.charAt(urlPart.length - 1) == '}';
    }

    function stripVariable(urlPart) {
        return urlPart.substr(1, urlPart.length - 2);
    }

    this.matches = function (url) {
        return !!self.extractArgs(url);
    };

    this.extractArgs = function (url) {
        var res = {};

        var query = extractQuery(url);
        url = trimQuery(url);
        var parts = splitParts(url);
        var templateParts = splitParts(templateUrl);
        for (var i = 0; i < templateParts.length; i++) {
            var templatePart = templateParts[i];
            var part = parts[i];
            if (!isVariable(templatePart)) {
                if (templatePart != part) {
                    return null;
                }
            } else {
                var key = stripVariable(templatePart);
                res[key] = decodeURIComponent(part);
            }
        }
        if (templateParts.length != parts.length) { //We can have {*rest} argument
            return null;
        }
        for (var queryKey in query) {
            res[queryKey] = query[queryKey];
        }
        return res;
    };

    this.format = function (args) {
        var query = {};
        var res = templateUrl;
        for (var arg in args) {
            var key = '{' + arg + '}';
            var value = args[arg];
            var encodedValue = encodeURIComponent(value);
            var argIndex = res.indexOf(key);
            if (argIndex >= 0) {
                res = res.replace(key, encodedValue);
            } else {
                query[arg] = encodedValue;
            }
        }
        var queryStr = '';
        for (var queryArg in query) {
            if (queryStr) queryStr += '&';
            queryStr += encodeURIComponent(queryArg) + '=' + query[queryArg];
        }
        if (queryStr) res = res + '?' + queryStr;
        return res;
    };

    this.formatHash = function (args) {
        return '#' + this.format(args);
    };

    this.navigateHash = function (args) {
        var url = this.formatHash(args);
        window.location.hash = url;
    };

    this.getName = function () {
        return name;
    };

    this.beforeLeave = function (args) {
        var ev = new $.Event('beforeLeave');
        dispatcher.trigger(ev, args);
        return !ev.isDefaultPrevented();
    };

    this.process = function (args, phash) {
        dispatcher.trigger('exec', [args, phash]);
    };

    this.addExecute = function (handler) {
        dispatcher.bind('exec', handler);
        return this;
    };

    this.addBeforeLeave = function (handler) {
        dispatcher.bind('beforeLeave', handler);
        return this;
    };

}